﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace WorkingFileManager
{
    /// <summary>
    /// Keeps track of what file the program is currently working with (if any), whether it is dirty,
    /// whether we should ask if the user wants to save changes before taking some action, etc.. Basically
    /// all of the logic related to the working file that would otherwise be spread out among many methods
    /// in the mainviewmodel.
    /// 
    /// Example use case:
    /// Notepad mainwindow creates a WorkingFileState in its constructor and keeps a member reference to it.
    /// In the mainwindow constructor, we also provide the value of FileStateOrWorkingFileChangedAction, like this:
    ///   wfm.FileStateOrWorkingFileChangedAction = () => { this.NotifyPropertyChanged("WindowTitle"); }
    /// At this point you may also want to persist the last working file name to a configuration storage.  
    /// 
    /// So whenever the WFM has a change, our notepad application will know to update its title.
    /// The WindowTitle property in our mainwindow viewmodel looks like this:
    /// 
    /// public string WindowTitle
    /// {
    ///    get
    ///    {
    ///       switch (wfm.FileState)
    ///       {
    ///         case WorkingFileState.FileStates.NoWorkingFile_Dirty:
    ///             return "Notepad - Untitled*";
    ///         case WorkingFileState.FileStates.NoWorkingFile_Clean:
    ///             return "Notepad - Untitled";
    ///         case WorkingFileState.FileStates.WorkingFile_Dirty:
    ///             return "Notepad - " + wfm.WorkingFileName + "*";
    ///         case WorkingFileState.FileStates.WorkingFile_Clean:
    ///             return "Notepad - " + wfm.WorkingFileName;
    ///       }
    ///    }
    /// }
    /// 
    /// we report actions that have taken place in the application to the WFM like this:
    /// 
    /// //new file
    /// workingFileManager.ReportPerformedFileEvent(WorkingFileState.PerformedFileEvents.New, filename: null);
    /// //file opened
    /// workingFileManager.ReportPerformedFileEvent(WorkingFileState.PerformedFileEvents.Open, fileName);
    /// //file saved
    /// workingFileManager.ReportPerformedFileEvent(WorkingFileState.PerformedFileEvents.Save, fileName);
    /// </summary>
    public class WorkingFileState : INotifyPropertyChanged
    {
        /// <summary>
        /// When a WorkingFileState is created, it is assumed that we have new working data
        /// that is clean.
        /// </summary>
        public WorkingFileState()
        {
            _FileState = FileStates.NoWorkingFile_Clean;
        }

        /// <summary>
        /// This action is executed when either the file state or the working file name changes.
        /// The code for this action will be supplied by the caller. Generally they will probably use
        /// this to update a WindowTitle property in a viewmodel, which will be bound to the actual title
        /// of an application window. That way, the caller can do things like set its title to 
        /// "c:\folder\file.file *" etc based on what the current file is and whether it is dirty.
        /// So generally the action code will be something like "NotifyPropertyChanged("MyApplicationWindowTitle");".
        /// </summary>
        public Action FileStateOrWorkingFileChangedAction
        {
            get;
            set;
        }

        /// <summary>
        /// Will be null when there is no working file.
        /// </summary>
        public string WorkingFileName
        {
            get
            {
                return _WorkingFileName;
            }
            private set
            {
                if (_WorkingFileName != value)
                {
                    _WorkingFileName = value;
                    NotifyPropertyChanged("WorkingFileName");
                    FileStateOrWorkingFileChangedAction();
                }
            }
        }
        private string _WorkingFileName;

        /// <summary>
        /// Indicates the state of the application - whether there is a working file and whether it is dirty.
        /// </summary>
        public FileStates FileState
        {
            get
            {
                return _FileState;
            }
            private set
            {
                if (_FileState != value)
                {
                    _FileState = value;
                    NotifyPropertyChanged("FileState");
                    FileStateOrWorkingFileChangedAction();
                }
            }
        }
        private FileStates _FileState;


        /// <summary>
        /// Indicates the current state of the working file.
        /// By "working file," we mean that there is a file name associated with what is being modified in the
        /// editor. For example, when you open Word, you start with a blank document, and that document has no name--no working
        /// file--until you save it. After that, you can press Ctrl-S and it knows where to save it without asking.
        /// Generally if working data is Clean then the user is not prompted to save before taking some other action.
        /// </summary>
        public enum FileStates
        {
            /// <summary>
            /// E.g., user pressed "New" or started up with a new file and then made some changes.
            /// </summary>
            NoWorkingFile_Dirty,

            /// <summary>
            /// E.g., user pressed "New" or started up with a new file and then made no changes.
            /// </summary>
            NoWorkingFile_Clean,

            /// <summary>
            /// E.g., user saved or opened a file and then made some more changes.
            /// </summary>
            WorkingFile_Dirty,

            /// <summary>
            /// E.g. user has just saved or opened a file but made no changes.
            /// </summary>
            WorkingFile_Clean
        }

        public enum PerformedFileEvents
        {
            /// <summary>
            /// Something in the content of the application's data has changed, causing it to become dirty.
            /// 
            /// </summary>
            Dirtied,

            /// <summary>
            /// Something has caused the application's data to go back to a clean state. This should NOT
            /// be used when a file is saved, it should only be used when something like an Undo has
            /// caused dirty data to become clean again.
            /// </summary>
            Cleaned,

            /// <summary>
            /// </summary>
            Save,

            /// <summary>
            /// </summary>
            New,

            /// <summary>
            /// </summary>
            Open,

            /// <summary>
            /// The caller can use this to indicate that the user tried to open a file but
            /// something went wrong. 
            /// This will tell us we need to remove it from the list of recent files if it's there.
            /// </summary>
            OpenFailed
        }

        /// <summary>
        /// Indicates if, given the current working file state,
        /// if the application should prompt the user to save changes.
        /// This should be checked when the user wants to open, save, or exit.
        /// </summary>
        public bool OpenNewExit_ShouldPromptSaveChanges()
        {
            switch (FileState)
            {
                case FileStates.NoWorkingFile_Dirty:
                case FileStates.WorkingFile_Dirty:
                    return true;
                case FileStates.NoWorkingFile_Clean:
                case FileStates.WorkingFile_Clean:
                    return false;
                default:
                    throw new Exception();
            }
        }

        public void ReportPerformedFileEvent(PerformedFileEvents fileEvent, string filename)
        {
            switch (fileEvent)
            {
                case PerformedFileEvents.Dirtied:
                    if (filename != null) throw new ArgumentException("fileName must be null when reporting a Dirtied event.");

                    if (FileState == FileStates.NoWorkingFile_Clean) FileState = FileStates.NoWorkingFile_Dirty;
                    else if (FileState == FileStates.WorkingFile_Clean) FileState = FileStates.WorkingFile_Dirty;
                    break;
                case PerformedFileEvents.Cleaned:
                    if (filename != null) throw new ArgumentException("fileName must be null when reporting a Cleaned event.");

                    if (FileState == FileStates.NoWorkingFile_Dirty) FileState = FileStates.NoWorkingFile_Clean;
                    else if (FileState == FileStates.WorkingFile_Dirty) FileState = FileStates.WorkingFile_Clean;
                    break;
                case PerformedFileEvents.Save:
                    if (filename == null) throw new ArgumentException("fileName must be given when reporting a Save event.");

                    //we update the filename no matter what the dirty state is
                    //the user could save a clean file to a new name
                    WorkingFileName = filename;
                    FileState = FileStates.WorkingFile_Clean;
                    AddToRecentFiles(filename);
                    break;
                case PerformedFileEvents.New:
                    if (filename != null) throw new ArgumentException("fileName must be null when reporting a New event.");

                    WorkingFileName = null;
                    FileState = FileStates.NoWorkingFile_Clean;
                    break;
                case PerformedFileEvents.Open:
                    if (filename == null) throw new ArgumentException("fileName must be given when reporting a Open event.");
                    WorkingFileName = filename;
                    FileState = FileStates.WorkingFile_Clean;
                    AddToRecentFiles(filename);
                    break;
                case PerformedFileEvents.OpenFailed:
                    RemoveFromRecentFiles(filename);
                    break;
            }
        }

        /// <summary>
        /// Contains a list of full file paths representing the files that have most recently been opened or saved to
        /// (i.e. that files that have been the current file). The most recent is at the beginning of this list.
        /// 
        /// </summary>
        private ObservableCollection<string> _RecentFiles = new ObservableCollection<string>();

        public ReadOnlyObservableCollection<string> RecentFiles
        {
            get
            {
                if (_roRecentFiles == null) _roRecentFiles = new ReadOnlyObservableCollection<string>(_RecentFiles);
                return _roRecentFiles;
            }
        }
        private ReadOnlyObservableCollection<string> _roRecentFiles;

        /// <summary>
        /// Adds a path to the recent files list. If the path is already present (even with different capitalization),
        /// it is moved to the top.
        /// Generally the recent files list is managed automatically when you save, open, and fail to open files, but
        /// we also need a way to populate it (e.g. from a config file) when the application is loaded. Therefore,
        /// this method is public.
        /// </summary>
        /// <param name="path"></param>
        public void AddToRecentFiles(string path)
        {
            RemoveFromRecentFiles(path);
            _RecentFiles.Insert(0, path);
        }

        /// <summary>
        /// Removes a path from the recent files list, even if it is present with different capitalization.
        /// </summary>
        /// <param name="path"></param>
        private void RemoveFromRecentFiles(string path)
        {
            _RecentFiles.RemoveAll(p => String.Compare(p, path, StringComparison.OrdinalIgnoreCase) == 0);
        }

        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
        #endregion

    }
}
